﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace MicroMVVM
{
    public class Container
    {
        /// <summary>
        /// The services container.
        /// </summary>
        private static readonly Dictionary<Type, Type> ServiceContainer;

        /// <summary>
        /// The services container.
        /// </summary>
        private static readonly Dictionary<Type, object> ServiceInstanceContainer;

        /// <summary>
        /// The view model container.
        /// </summary>
        private static readonly Dictionary<string, Type> ViewModelContainer;

        static Container()
        {
            ServiceContainer = ServiceContainer ?? new Dictionary<Type, Type>();
            ServiceInstanceContainer = ServiceInstanceContainer ?? new Dictionary<Type, object>();
            ViewModelContainer = ViewModelContainer ?? new Dictionary<string, Type>();
        }

        /// <summary>
        /// Registers Service.
        /// </summary>
        /// <typeparam name="TInterface">Interface Type</typeparam>
        /// <typeparam name="TClass">Class Type</typeparam>
        public static void RegisterService<TInterface, TClass>()
        {
            var tInterface = typeof (TInterface);
            var tClass = typeof (TClass);

            RegisterService(tInterface, tClass);
        }

        /// <summary>
        /// Registers Service.
        /// </summary>
        /// <param name="tInterface">Interface Type</param>
        /// <param name="tClass">Class Type</param>
        public static void RegisterService(Type tInterface, Type tClass)
        {
            if (ServiceContainer.ContainsKey(tInterface)) return;

            ServiceContainer.Add(tInterface, tClass);
        }

        /// <summary>
        /// Maps View Model to a string identifier.
        /// </summary>
        /// <typeparam name="T">View Model type</typeparam>
        /// <param name="identifier">View Model identifier</param>
        public static void MapViewModel<T>(string identifier) where T : class
        {
            var type = typeof (T);

            MapViewModel(identifier, type);
        }

        /// <summary>
        /// Maps View Model to a string identifier.
        /// </summary>
        /// <param name="identifier">View Model identifier</param>
        /// <param name="viewModel">View Model type</param>
        public static void MapViewModel(string identifier, Type viewModel)
        {
            if (ViewModelContainer.ContainsKey(identifier)) return;

            ViewModelContainer.Add(identifier, viewModel);
        }

        /// <summary>
        /// Resolves Service.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static T ResolveService<T>() where T : class
        {
            var type = typeof (T);

            return (T) ResolveService(type, false);
        }

        /// <summary>
        /// Resolves Service.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static T ResolveService<T>(bool isInDesignMode) where T : class
        {
            var type = typeof (T);

            return (T) ResolveService(type, isInDesignMode);
        }

        /// <summary>
        /// Resolves the Service.
        /// </summary>
        /// <param name="tInterface">Service type</param>
        /// <returns></returns>
        private static object ResolveService(Type tInterface)
        {
            return ResolveService(tInterface, false);
        }

        /// <summary>
        /// Resolves the Service.
        /// </summary>
        /// <param name="tInterface">Service type</param>
        /// <param name="isInDesignMode">Indicated whether the application is in design mode</param>
        /// <returns></returns>
        private static object ResolveService(Type tInterface, bool isInDesignMode)
        {
            if (isInDesignMode) return null;

            var service = ServiceContainer.ContainsKey(tInterface)
                              ? ServiceContainer[tInterface]
                              : null;

            if (service == null)
            {
                System.Diagnostics.Debug.WriteLine(string.Format("Error: The service with interface {0} was not found!", tInterface));

                return null;
            }

            ExportService exportService;

            InternalHelpers.TryGetAttribute(service.GetTypeInfo(), out exportService);

            if (exportService == null)
            {
                System.Diagnostics.Debug.WriteLine(string.Format("Error: Failed locating export service for {0}!", tInterface));

                return null;
            }

            var partCreationPolicy = exportService.PartCreationPolicy;

            if ((partCreationPolicy == PartCreationPolicy.Singleton) &&
                (ServiceInstanceContainer.ContainsKey(tInterface)))
                return ServiceInstanceContainer[tInterface];

            var constructor = service.GetTypeInfo().DeclaredConstructors.FirstOrDefault();

            if (constructor == null) return null;

            var parameterTypes = constructor.GetParameters().Select(p => p.ParameterType).ToList();

            var instance = Instantiate(service, parameterTypes);

            if ((partCreationPolicy == PartCreationPolicy.Singleton) &&
                (ServiceInstanceContainer.ContainsKey(tInterface) == false))
                ServiceInstanceContainer.Add(tInterface, instance);

            return instance;
        }

        /// <summary>
        /// Resolves View Model.
        /// </summary>
        /// <typeparam name="T">View Model Type</typeparam>
        /// <returns></returns>
        public static object ResolveViewModel<T>()
        {
            var type = typeof (T);

            return ResolveViewModel(type.Name, false);
        }

        /// <summary>
        /// Resolves View Model.
        /// </summary>
        /// <typeparam name="T">View model type</typeparam>
        /// <param name="isInDesignMode">Is design mode</param>
        /// <returns></returns>
        public static object ResolveViewModel<T>(bool isInDesignMode)
        {
            var type = typeof(T);

            return ResolveViewModel(type.Name, isInDesignMode);
        }

        /// <summary>
        /// Resolves View Model.
        /// </summary>
        /// <param name="identifier"></param>
        /// <returns></returns>
        public static object ResolveViewModel(string identifier)
        {
            return ResolveViewModel(identifier, false);
        }

        /// <summary>
        /// Resolves View Model.
        /// </summary>
        /// <param name="identifier">View model identifier</param>
        /// <param name="isInDesignMode">Is design mode</param>
        /// <returns></returns>
        public static object ResolveViewModel(string identifier, bool isInDesignMode)
        {
            if (isInDesignMode) return new object();

            var viewModel = ViewModelContainer.ContainsKey(identifier)
                                ? ViewModelContainer[identifier]
                                : null;

            if (viewModel == null)
            {
                System.Diagnostics.Debug.WriteLine(string.Format("Error: The ViewModel with identifier {0} was not found!", identifier));

                return null;
            }

            var importingConstructor = viewModel.GetTypeInfo().DeclaredConstructors.FirstOrDefault(c =>
                                                                 {
                                                                     ImportingConstructor constructor;

                                                                     return (InternalHelpers.TryGetAttribute(c, out constructor));
                                                                 });

            if (importingConstructor == null)
            {
                System.Diagnostics.Debug.WriteLine(string.Format("Warning: Failed locating ImportingConstructor for ViewModel {0}! Attempting to instantiate ViewModel with empty constructor.", identifier));

                var emptyConstructor = viewModel.GetTypeInfo().DeclaredConstructors.FirstOrDefault(c => c.GetParameters().Any() == false);

                if (emptyConstructor == null)
                {
                    System.Diagnostics.Debug.WriteLine(string.Format("Error: Failed locating empty constructor for ViewModel {0}!", identifier));

                    return null;
                }

                return Instantiate(viewModel, null);
            }

            var parameterTypes = importingConstructor.GetParameters().Select(p => p.ParameterType).ToList();

            return Instantiate(viewModel, parameterTypes);
        }

        /// <summary>
        /// Creates instance of the parametrised Type.
        /// </summary>
        /// <param name="tClass"></param>
        /// <param name="arguments"></param>
        /// <returns></returns>
        private static object Instantiate(Type tClass, params object[] arguments)
        {
            System.Diagnostics.Debug.WriteLine(string.Format("Instantiating {0}.", tClass));

            if ((arguments == null) || (arguments.Any() == false)) return Activator.CreateInstance(tClass);

            try
            {
                var parameters = ResolveParameters(arguments).ToArray();

                return Activator.CreateInstance(tClass, parameters);
            }
            catch (Exception)
            {
                System.Diagnostics.Debug.WriteLine(string.Format("Error: Failed instantiating {0}, parameter mismatch!", tClass));

                return null;
            }
        }

        /// <summary>
        /// Retrieves the parameter types.
        /// </summary>
        /// <param name="arguments"></param>
        /// <returns></returns>
        private static IEnumerable<object> ResolveParameters(params object[] arguments)
        {
            if ((arguments == null) || (arguments.Length == 0)) yield break;

            var parameterList = arguments.FirstOrDefault();

            if (parameterList == null) yield break;

            var parameters = parameterList as IList<Type>;

            if ((parameters == null) || (parameters.Count == 0)) yield break;

            foreach (var obj in parameters.Select(ResolveService).Where(obj => obj != null))
            {
                System.Diagnostics.Debug.WriteLine(string.Format("Resolving parameter {0}.", obj));

                yield return obj;
            }
        }
    }
}